
#include <ApplicationServices/ApplicationServices.h>
#import <Foundation/Foundation.h>
#import "MouseAndCatAppDelegate.h"
#include "Event.h"

@implementation MouseAndCatAppDelegate

@synthesize window;
@synthesize udpClient, udpServer;

static CGRect screenBounds;
static const char* kServerIp = "192.168.1.105";
static const int kServerPort = 8989;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    screenBounds = CGDisplayBounds(CGMainDisplayID()); 
    
    //_mouseTrackThread = [[[NSThread alloc] initWithTarget:self selector:@selector(startMouseTracking) object:nil] retain];
    _serverThread =     [[[NSThread alloc] initWithTarget:self selector:@selector(startServer) object:nil] retain];
    _clientThread =     [[[NSThread alloc] initWithTarget:self selector:@selector(startClient) object:nil] retain];

    //[_mouseTrackThread start];
    [_serverThread start];
    [_clientThread start];
}

-(void)startMouseTracking
{
    printf("The main screen is %dx%d\n", (int)screenBounds.size.width, (int)screenBounds.size.height);
    
    CFMachPortRef eventTap; 
    CGEventMask eventMask; 
    CFRunLoopSourceRef runLoopSource; // The screen size of the primary display. 
    
    // Create an event tap. We are interested in mouse movements. 
    eventMask = (1 << kCGEventMouseMoved); 
    eventTap = CGEventTapCreate( kCGSessionEventTap, kCGHeadInsertEventTap, 0, eventMask, myCGEventCallback, NULL);
    runLoopSource = CFMachPortCreateRunLoopSource( kCFAllocatorDefault, eventTap, 0);
    
    // Add to the current run loop. 
    CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); 
    // Enable the event tap. 
    CGEventTapEnable(eventTap, true); 
    // Set it all running. 
    CFRunLoopRun();

}

-(void)startClient
{
    self.udpClient = [[UDPClient alloc] initWithDelegate:self];
    NSString* serverIp = [[NSString alloc] initWithCString:kServerIp]; 
    [self.udpClient runClientWithHost:serverIp port:kServerPort];
    NSLog(@"Started UDP client");
}

-(void)startServer
{
    self.udpServer = [[UDPClient alloc] initWithDelegate:self];
    [self.udpServer runServerOnPort:8989];
    NSLog(@"Started UDP server");
}

CGEventRef myCGEventCallback(CGEventTapProxy proxy ,CGEventType type, CGEventRef event, void* refcon)
{ 
    if (type != kCGEventMouseMoved)
        return event; // The incoming mouse position. 
    //CGPoint location = CGEventGetLocation(event); // We can change aspects of the mouse event. // For example, we can use CGEventSetLoction(event, newLocation). 

    
    // We must return the event for it to be useful. 
    return event; 
}

-(void) setMouseEvent: (CGEventType)type :(CGPoint)point
{
    CGEventRef theEvent = CGEventCreateMouseEvent(NULL, type, point, 0);
    //CGEventSetType(theEvent, type);
    CGEventPost(kCGHIDEventTap, theEvent);
    CFRelease(theEvent);
}

-(void) setKeyboardEvent: (CGKeyCode)keyCode :(BOOL)keyDown
{
    CGEventRef theEvent = CGEventCreateKeyboardEvent(NULL, keyCode, keyDown);
    CGEventPost(kCGHIDEventTap, theEvent);
    CFRelease(theEvent);
}

-(CGPoint) getMouseLocation
{
    CGEventRef dummyEvent = CGEventCreate(NULL);
    CGPoint mouseLoc = CGEventGetLocation(dummyEvent);
    return mouseLoc;
}

-(void) receivedData:(NSData *)data
{
    Event event = EventParse([data bytes]);
    CGPoint mouseLoc = [self getMouseLocation];

    if(!_beSlave)
    {
        if(event.type == MouseEnter)
        {
            NSLog(@"Mouse entered: %d, %d", event.x, event.y);
            
            _xEntry = event.x;
            
            if(event.x == left)
                event.x = 0;
            else
                event.x = screenBounds.size.width;
            
            // Translate from percentage of screen height to actual pixels
            event.y = ((float)event.y / 100) * screenBounds.size.height;
            
            NSLog(@"Set X entry: %u", _xEntry);
            _beSlave = YES;
            [self setMouseEvent: kCGEventMouseMoved :CGPointMake(event.x, event.y)];
        }
        return;
    }
    
    if(event.type == KeyDown || event.type == KeyUp)
    {
        UInt32 latest = event.type == KeyDown ? _maxTimeKeyboardDown : _maxTimeKeyboardUp;
        
        if(event.timestamp < latest)
        {
            NSLog(@"Discarding %u data with old timestamp: %u should be at least %u", event.type, event.timestamp, latest);
            return;
        }
        
        switch (event.type) 
        {
            case KeyDown:
                _maxTimeKeyboardDown = MAX(_maxTimeKeyboardDown, event.timestamp);
                break;
            case KeyUp:
                _maxTimeKeyboardUp = MAX(_maxTimeKeyboardUp, event.timestamp);         
            default:
                break;
        }
        
        //NSLog(@"Posting key event: %u", event.keyCode);
        [self setKeyboardEvent: (CGKeyCode)event.keyCode: event.type == KeyDown];
        
        return;
    }

    switch (event.type)
    {
        case MouseMove:
            _mouseMoveDiscardedEventCount++;
            
            if(event.timestamp < _mouseMoveEventcount)
            {
                _mouseMoveDiscardedEventCount ++;
                NSLog(@"Discarding %u of %u events of type %u due to timestamp: %u (should be at least %u)", _mouseMoveDiscardedEventCount, _mouseMoveEventcount, event.type, event.timestamp, _maxTimeMouseMove);
                return;
            }
            _maxTimeMouseMove = event.timestamp;
        
            if((_xEntry == left && mouseLoc.x == 0 && event.x < 0) || 
               (_xEntry == right && mouseLoc.x == screenBounds.size.width && event.x > 0))
            {
                NSLog(@"Mouse leave at: %f, %f", mouseLoc.x, mouseLoc.y);
                
                UInt8 data[2];
                //SInt16 sy = (SInt16)mouseLoc.y;
                data[0] = MouseLeave;
                data[1] = (UInt8)((mouseLoc.y / screenBounds.size.height) * 100);
                //data[1] = sy & 0xFF;
                //data[2] = (sy >> 8) & 0xFF;
                const UInt8 *dataStart = data;
                [self.udpClient send:dataStart :2];
                _beSlave = NO;
                //NSLog(@"Finished mouse leave process with bytes: %d, %d", data[1], data[2]);
                [self setMouseEvent: kCGEventMouseMoved : CGPointMake(screenBounds.size.width-1, screenBounds.size.height-1)];
                
                NSLog(@"Finished mouse leave process with bytes: %d, %d", data[0], data[1]);

                return;
            }
            
            mouseLoc.x += event.x;
            mouseLoc.y += event.y;
            
            if(mouseLoc.x < 0)
                mouseLoc.x = 0;
            
            if(mouseLoc.x > screenBounds.size.width)
                mouseLoc.x = screenBounds.size.width;
    
            if(mouseLoc.y < 0)
                mouseLoc.y = 0;
            
            if(mouseLoc.y > screenBounds.size.height)
            {
                NSLog(@"moving mouse Y inbounds %f -> %f", mouseLoc.y, screenBounds.size.height);
                mouseLoc.y = screenBounds.size.height;
            }
    
            //NSLog(@"Setting mouse move event: %f, %f", mouseLoc.x, mouseLoc.y);
            [self setMouseEvent: _mouseDown ? kCGEventLeftMouseDragged : kCGEventMouseMoved : mouseLoc];
            break;
            
        case MouseWheel:
            
            if(event.timestamp < _maxTimeMouseWheel)
            {
                NSLog(@"Discarding %u data with old timestamp: %u (should be at least %u", event.type, event.timestamp, _maxTimeMouseWheel);
                return;
            }
            _maxTimeMouseWheel = event.timestamp;
            CGEventRef theEvent = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 1, event.y);
            CGEventPost(kCGHIDEventTap, theEvent);
            CFRelease(theEvent);

            break;
            
        case MouseDown:
            
            //NSLog(@"Mouse down");
            if(event.timestamp < _maxTimeMouseDown)
            {
                NSLog(@"Discarding %u data with old timestamp: %u (should be at least %u", event.type, event.timestamp, _maxTimeMouseDown);
                return;
            }
            _maxTimeMouseDown = event.timestamp;

            [self setMouseEvent: kCGEventLeftMouseDown : mouseLoc];
            _mouseDown = YES;
            break;
            
        case MouseUp:
            
            //NSLog(@"Mouse Up");
            if(event.timestamp < _maxTimeMouseUp)
            {
                NSLog(@"Discarding %u data with old timestamp: %u (should be at least %u", event.type, event.timestamp, _maxTimeMouseUp);
                return;
            }
            _maxTimeMouseUp = event.timestamp;

            [self setMouseEvent: kCGEventLeftMouseUp : mouseLoc];
            _mouseDown = NO;
            break;
        
        case LeftMouseDoubleClick:
            
            if(event.timestamp < _maxTimeLeftMouseDoubleClick)
            {
                NSLog(@"Discarding %u data with old timestamp: %u (should be at least %u", event.type, event.timestamp, _maxTimeLeftMouseDoubleClick);
                return;
            }
            _maxTimeLeftMouseDoubleClick = event.timestamp;
            
            CGEventRef dblClick = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, mouseLoc, 0);
            CGEventSetIntegerValueField(dblClick, kCGMouseEventClickState, 2);
            //CGEventSetType(theEvent, type);
            CGEventPost(kCGHIDEventTap, dblClick);
            CFRelease(dblClick);
            
            //[self setMouseEvent: kCGEventLeftMouseDown : mouseLoc];
            [self setMouseEvent: kCGEventLeftMouseUp : mouseLoc];
            
            _mouseDown = NO;
            break;
            
        case RightMouseDown:
            
            if(event.timestamp < _maxTimeRightMouseDown)
            {
                NSLog(@"Discarding %u data with old timestamp: %u (should be at least %u", event.type, event.timestamp, _maxTimeRightMouseDown);
                return;
            }
            _maxTimeRightMouseDown = event.timestamp;

            [self setMouseEvent: kCGEventRightMouseDown : mouseLoc];

            break;
        
        case RightMouseUp:
            
            if(event.timestamp < _maxTimeRightMouseUp)
            {
                NSLog(@"Discarding %u data with old timestamp: %u (should be at least %u", event.type, event.timestamp, _maxTimeRightMouseUp);
                return;
            }
            _maxTimeRightMouseUp = event.timestamp;
            
            [self setMouseEvent: kCGEventRightMouseUp : mouseLoc];
            
            break;
            
        default:
            
            NSLog(@"Unknown event received: %d", event.type);
            return;
            break;
    }
}



- (NSURL *)applicationFilesDirectory {

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *libraryURL = [[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject];
    return [libraryURL URLByAppendingPathComponent:@"MouseAndCat"];
}

/**
    Creates if necessary and returns the managed object model for the application.
 */
- (NSManagedObjectModel *)managedObjectModel {
    if (__managedObjectModel) {
        return __managedObjectModel;
    }
	
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"MouseAndCat" withExtension:@"momd"];
    __managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];    
    return __managedObjectModel;
}

/**
    Returns the persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.)
 */
- (NSPersistentStoreCoordinator *) persistentStoreCoordinator {
    if (__persistentStoreCoordinator) {
        return __persistentStoreCoordinator;
    }

    NSManagedObjectModel *mom = [self managedObjectModel];
    if (!mom) {
        NSLog(@"%@:%@ No model to generate a store from", [self class], NSStringFromSelector(_cmd));
        return nil;
    }

    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *applicationFilesDirectory = [self applicationFilesDirectory];
    NSError *error = nil;
    
    NSDictionary *properties = [applicationFilesDirectory resourceValuesForKeys:[NSArray arrayWithObject:NSURLIsDirectoryKey] error:&error];
        
    if (!properties) {
        BOOL ok = NO;
        if ([error code] == NSFileReadNoSuchFileError) {
            ok = [fileManager createDirectoryAtPath:[applicationFilesDirectory path] withIntermediateDirectories:YES attributes:nil error:&error];
        }
        if (!ok) {
            [[NSApplication sharedApplication] presentError:error];
            return nil;
        }
    }
    else {
        if ([[properties objectForKey:NSURLIsDirectoryKey] boolValue] != YES) {
            // Customize and localize this error.
            NSString *failureDescription = [NSString stringWithFormat:@"Expected a folder to store application data, found a file (%@).", [applicationFilesDirectory path]]; 
            
            NSMutableDictionary *dict = [NSMutableDictionary dictionary];
            [dict setValue:failureDescription forKey:NSLocalizedDescriptionKey];
            error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:101 userInfo:dict];
            
            [[NSApplication sharedApplication] presentError:error];
            return nil;
        }
    }
    
    NSURL *url = [applicationFilesDirectory URLByAppendingPathComponent:@"MouseAndCat.storedata"];
    __persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
    if (![__persistentStoreCoordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:nil error:&error]) {
        [[NSApplication sharedApplication] presentError:error];
        [__persistentStoreCoordinator release], __persistentStoreCoordinator = nil;
        return nil;
    }

    return __persistentStoreCoordinator;
}

/**
    Returns the managed object context for the application (which is already
    bound to the persistent store coordinator for the application.) 
 */
- (NSManagedObjectContext *) managedObjectContext {
    if (__managedObjectContext) {
        return __managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        [dict setValue:@"Failed to initialize the store" forKey:NSLocalizedDescriptionKey];
        [dict setValue:@"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey];
        NSError *error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
        [[NSApplication sharedApplication] presentError:error];
        return nil;
    }
    __managedObjectContext = [[NSManagedObjectContext alloc] init];
    [__managedObjectContext setPersistentStoreCoordinator:coordinator];

    return __managedObjectContext;
}

/**
    Returns the NSUndoManager for the application. In this case, the manager returned is that of the managed object context for the application.
 */
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window {
    return [[self managedObjectContext] undoManager];
}

/**
    Performs the save action for the application, which is to send the save: message to the application's managed object context. Any encountered errors are presented to the user.
 */
- (IBAction) saveAction:(id)sender {
    NSError *error = nil;
    
    if (![[self managedObjectContext] commitEditing]) {
        NSLog(@"%@:%@ unable to commit editing before saving", [self class], NSStringFromSelector(_cmd));
    }

    if (![[self managedObjectContext] save:&error]) {
        [[NSApplication sharedApplication] presentError:error];
    }
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {

    // Save changes in the application's managed object context before the application terminates.

    if (!__managedObjectContext) {
        return NSTerminateNow;
    }

    if (![[self managedObjectContext] commitEditing]) {
        NSLog(@"%@:%@ unable to commit editing to terminate", [self class], NSStringFromSelector(_cmd));
        return NSTerminateCancel;
    }

    if (![[self managedObjectContext] hasChanges]) {
        return NSTerminateNow;
    }

    NSError *error = nil;
    if (![[self managedObjectContext] save:&error]) {

        // Customize this code block to include application-specific recovery steps.              
        BOOL result = [sender presentError:error];
        if (result) {
            return NSTerminateCancel;
        }

        NSString *question = NSLocalizedString(@"Could not save changes while quitting. Quit anyway?", @"Quit without saves error question message");
        NSString *info = NSLocalizedString(@"Quitting now will lose any changes you have made since the last successful save", @"Quit without saves error question info");
        NSString *quitButton = NSLocalizedString(@"Quit anyway", @"Quit anyway button title");
        NSString *cancelButton = NSLocalizedString(@"Cancel", @"Cancel button title");
        NSAlert *alert = [[NSAlert alloc] init];
        [alert setMessageText:question];
        [alert setInformativeText:info];
        [alert addButtonWithTitle:quitButton];
        [alert addButtonWithTitle:cancelButton];

        NSInteger answer = [alert runModal];
        [alert release];
        alert = nil;
        
        if (answer == NSAlertAlternateReturn) {
            return NSTerminateCancel;
        }
    }

    return NSTerminateNow;
}

- (void)dealloc
{
    [__managedObjectContext release];
    [__persistentStoreCoordinator release];
    [__managedObjectModel release];
    
    [_clientThread release];
    [_serverThread release];
    [_mouseTrackThread release];
    [super dealloc];
}

@end
